namespace SampleModel;

class MysqlAccessor
{
    const OP_GREATER_THAN = ">";
    const OP_GREATER_EQUAL = ">=";
    const OP_LESS_THAN = "<";
    const OP_LESS_EQUAL = "<=";
    const OP_EQUAL = "=";
    const OP_NOT_EQUAL = "<>";
    const OP_IN = "IN";
    const OP_NOT_IN = "NOT IN";
    const OP_LIKE = "LIKE";
    const OP_NOT_LIKE = "NOT LIKE";

    public connection = null;
    protected static connections = null;

    private splitSuffix;
    private modelName;
    private mapping;

    public forceMaster = false;
    public readOnly = true;

    private filterNames;
    private filterOperations;
    private filterValues;

    private offset = 0;
    private rowCount = 0;
    private sorts;
    private distinct = false;
    private setFields;

    private pdoValues;

    private sql;

    private function __construct()
    {
        var connections;
        let this->pdoValues = [];
        let this->sorts = [];
        let this->setFields = [];
        let this->filterValues = [];
        let this->filterOperations = [];
        let this->filterNames = [];

        let connections = self::connections;
        if is_null(connections) {
            let self::connections = [];
        }
    }

    public static function useModel(string! modelName, string splitSuffix = "") -> <MysqlAccessor>
    {
        var dataAccessor;
        let dataAccessor = new self();
        let dataAccessor->splitSuffix = splitSuffix;
        let dataAccessor->modelName = modelName;

        let dataAccessor->mapping ={modelName}::getMapping(splitSuffix);

        return dataAccessor;
    }

    public function forceMaster(boolean forceMaster = true) -> <MysqlAccessor>
    {
        let this->forceMaster = forceMaster;
        return this;
    }

    public function distinct() -> <MysqlAccessor>
    {
        let this->distinct = true;
        return this;
    }

    public function delete() -> int
    {
        var tableName;

        let tableName = this->mapping["table"];
        let this->sql = "DELETE FROM `" . tableName . "`";

        this->appendWhereCondition();
        this->appendLimit();
        return this->executeWrite();
    }

    public function sort(string! field, boolean! desc = true) -> <MysqlAccessor>
    {
        let this->sorts[field] = desc ? "DESC" : "ASC";
        return this;
    }

    public function setField(string! field, value) -> <MysqlAccessor>
    {
        let this->setFields[field] = value;
        return this;
    }

    public function update() -> int
    {
        var mapping;
        let mapping = this->mapping;

        let this->sql = "UPDATE `". mapping["table"] ."`";

        this->appendUpdate();
        this->appendWhereCondition();
        return this->executeWrite();
    }

    public function insert(string! type = "INSERT") -> int
    {
        var mapping;
        let mapping = this->mapping;
        let this->sql = strtoupper(type) . " INTO `" . mapping["table"] . "`";
        this->appendInsert();
        return $this->executeWrite();
    }

    protected function appendInsert() -> void
    {
        var field, columns, holder, columnMapping, setFields, pdoValues;

        let columns = [];

        let columnMapping = this->mapping["columns"];
        let pdoValues = this->pdoValues;
        let setFields = this->setFields;

        if count(setFields) == 0 {
            throw new \Exception("Set fields can't be empty.");
        }

        for field, _ in setFields {
            let columns[] = columnMapping[field];
        }

        let holder = array_fill(0, count(columns), "?");

        let this->sql = this->sql . "(`". implode("`, `", columns) ."`) VALUES (" . implode(", ", holder) . ")";
        let this->pdoValues = array_merge(pdoValues, array_values(setFields));
    }

    protected function appendUpdate() -> void
    {
        var field, columnMapping, setFields, setFragment, pdoValues;

        let setFragment = [];
        let setFields = this->setFields;
        let columnMapping = this->mapping["columns"];
        let pdoValues = this->pdoValues;

        if count(setFields) == 0 {
            throw new \Exception("Set fields can't be empty.");
        }

        for field, _ in setFields {
            let setFragment[] = "`" . columnMapping[field] . "` = ?";
        }

        let this->pdoValues = array_merge(pdoValues, array_values(setFields));
        let this->sql = this->sql . " SET " . implode(", ", setFragment);
    }

    public function find(array! selectedFields = []) -> array
    {
        var fields, columnMapping, mapping, field, distinct, sql, result, pdoStatement, rows, modelName, splitSuffix, fieldMapping, data, key, value, row;

        let fields = [];
        let mapping = this->mapping;
        let columnMapping = mapping["columns"];
        let fieldMapping = [];
        if count(selectedFields) > 0 {
            for field in selectedFields {
                let fields[] = columnMapping[field];
                let fieldMapping[field] = columnMapping[field];
            }
        } else {
            let fields = array_values(columnMapping);
            let fieldMapping = array_combine(array_values(columnMapping),array_keys(columnMapping));
        }

        let distinct = this->distinct;

        if distinct {
            let sql = "SELECT DISTINCT `" . implode("`, `", fields) . "` FROM `" . mapping["table"] . "`";
        } else {
            let sql = "SELECT `" . implode("`, `", fields) . "` FROM `" . mapping["table"] . "`";
        }

        let this->sql = sql;

        this->appendWhereCondition();
        this->appendOrder();
        this->appendLimit();

        let result = [];
        let modelName = this->modelName;
        let splitSuffix = this->splitSuffix;

        let pdoStatement = this->executeRead();
        let rows = pdoStatement->fetchAll(\PDO::FETCH_ASSOC);


        for row in rows {
            let data = [];
            for key, value in row {
                let key = fieldMapping[key];
                let data[key] = value;
            }
            let result[] = {modelName}::create(data, splitSuffix, true);
        }

        return result;
    }

    protected function appendOrder() -> void
    {
        var sorts, columnMapping, temp, field, sortType;

        let sorts = this->sorts;
        let columnMapping = this->mapping["columns"];

        if count(sorts) == 0 {
            return;
        }

        let temp = [];
        for field, sortType in sorts {
            let temp[] = "`" . columnMapping[field] . "` " . sortType;
        }

        let this->sql = this->sql . " ORDER BY " . implode(", ", temp);
    }

    protected function appendWhereCondition() -> void
    {
        var whereCondition, filterNames, filterOperations, filterOperation, filterValues, filterValue, columns, valueString, pdoValues, index, field;

        let filterNames = this->filterNames;
        let filterOperations = this->filterOperations;
        let filterValues = this->filterValues;
        let columns = this->mapping["columns"];
        let whereCondition = [];

        if !fetch pdoValues, this->pdoValues {
            let pdoValues = [];
        }

        if empty filterNames || empty filterOperations || empty filterValues {
            return ;
        }

        for index, field in filterNames {
            let filterValue = filterValues[index];
            let filterOperation = filterOperations[index];
            if is_array(filterValue) {
                let valueString = implode(", ", array_fill(0, count(filterValue), "?"));
                let whereCondition[] = "`" . columns[field] . "` " . filterOperation . "(" . valueString . ")";
                let pdoValues = array_merge(pdoValues, filterValue);
            } else {
                let whereCondition[] = "`" . columns[field] . "` " . filterOperation . "?";
                let pdoValues[] = filterValue;
            }
        }
        let this->pdoValues = pdoValues;
        let this->sql = this->sql . " WHERE " . implode(" AND ", whereCondition);
    }

    protected function appendLimit() -> void
    {
        var offset, rowCount, limit;

        let offset = this->offset;
        let rowCount = this->rowCount;

        if offset > 0 {
            let limit = " LIMIT " . offset . ", " . rowCount;
        } elseif rowCount > 0 {
            let limit = " LIMIT " . rowCount;
        }else{
            return ;
        }

        let this->sql = this->sql . limit;
    }

    protected function executeWrite() -> int
    {
        var statement, effectedRowCount;
        let this->readOnly = false;
        let statement = this->executeInternal();
        let effectedRowCount = statement->rowCount();
        return effectedRowCount;
    }

    protected function executeRead() -> <\PDOStatement>
    {
        let this->readOnly = true;
        return this->executeInternal();
    }

    protected function prepare() -> <\PDOStatement>
    {
        var sql, connection, statement;

        let sql = this->sql;
        let connection = <\PDO> this->getConnection();
        let statement = <\PDOStatement> connection->prepare(sql);

        return statement;
    }

    protected function executeInternal() -> <\PDOStatement>
    {
        var statement, executeResult, errorInfo, exceptionMsg, values;

        let values = this->pdoValues;
        let statement = this->prepare();
        let executeResult = statement->execute(values);

        if executeResult == false {
            let errorInfo = this->getErrorInfo(statement);
            let exceptionMsg = sprintf("[%d][%d] %s", errorInfo[0], errorInfo[1], errorInfo[2]);
            throw new \Exception(exceptionMsg);
        }

        return statement;
    }

    protected function getErrorInfo(<\PDOStatement> statement) -> array
    {
        return statement->errorInfo();
    }

    protected function getConnection() -> <\PDO>
    {
        var modelName, pdoConfig, pdoConfigName, dsn, userName, password, driverOptions, connection, connections;

        let connection = this->connection;
        if !is_null(connection) {
            return connection;
        }

        let connections = self::connections;
        let modelName = this->modelName;
        let pdoConfigName = {modelName}::getPdoConfig(this);

        if fetch connection, connections[pdoConfigName] {
            let this->connection = connection;
            return connection;
        }

        let pdoConfig = Config::get(pdoConfigName);

        if !fetch dsn, pdoConfig["dsn"] || !fetch userName, pdoConfig["userName"] || !fetch password, pdoConfig["password"] {
            throw new \Exception(modelName . "::getPdoConfig return error.");
        }

        if fetch driverOptions, pdoConfig["driverOptions"] {
            let connection = new \PDO(dsn, userName, password, driverOptions);
        } else {
            let connection = new \PDO(dsn, userName, password);
        }

        let this->connection = connection;
        let connections[pdoConfigName] = connection;
        let self::connections = connections;
        return connection;
    }

    public function filter(name, value) -> <MysqlAccessor>
    {
        var operation;
        let operation = self::OP_EQUAL;
        if (is_array(value)) {
            let operation = self::OP_IN;
        }

        return this->filterByOperation(name, operation, value);
    }

    public function filterByOperation(string! field, string! operation, value) -> <MysqlAccessor>
    {
        let this->filterNames[] = field;
        let this->filterOperations[] = operation;
        let this->filterValues[] = value;

        return this;
    }

    public function limit(int! rowCount, int! offset = 0) -> <MysqlAccessor>
    {
        let this->rowCount = rowCount;
        if offset > 0 {
            let this->offset = offset;
        }

        return this;
    }

    public static function closeConnections() -> void
    {
        var connection, connections;
        let connections = self::connections;

        for connection in connections {
            connection->close();
        }
    }
}